package com.maikang.exammall.service;

import com.maikang.exammall.entity.BaseEntity;
import com.maikang.exammall.entity.action.Insert;
import com.maikang.exammall.repository.BaseRepository;
import com.maikang.exammall.util.BeanUtils;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.validation.Valid;
import javax.validation.groups.Default;
import java.io.Serializable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;

import static com.maikang.exammall.util.BeanUtils.getNullPropertyNames;

public class BaseService<ENTITY extends BaseEntity<ID>, ID extends Serializable, REPOSITORY extends BaseRepository<ENTITY, ID> & JpaSpecificationExecutor<ENTITY>> implements
        CrudRepository<ENTITY, ID>, QueryByExampleExecutor<ENTITY>, JpaSpecificationExecutor<ENTITY> {

    @Autowired
    protected REPOSITORY repository;


    /**
     * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
     * entity instance completely.
     *
     * @param entity must not be {@literal null}.
     * @return the saved entity; will never be {@literal null}.
     * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
     */
    @SneakyThrows
    @Transactional(rollbackFor = Exception.class)
    @Override
    public <S extends ENTITY> S save(S entity) {
        S target;
        ID id = entity.getPrimaryKey();
        if (id == null) {
            GeneratedValue generatedValue = null;
            try {
                generatedValue = entity.getClass().getDeclaredField("id").getAnnotation(GeneratedValue.class);
            } catch (NoSuchFieldException | SecurityException ignore) {
            }
            if (generatedValue != null && generatedValue.strategy() != GenerationType.AUTO && generatedValue.strategy() != GenerationType.IDENTITY) {
                entity.setPrimaryKey((ID) UUID.randomUUID().toString().replaceAll("-", ""));
            }
        }

        if (id != null) { // update
            target = (S) this.findById(id).orElse(entity);
            BeanUtils.copyProperties(entity, target, getNullPropertyNames(entity));
        } else { // insert
            target = entity;
        }
        return repository.save(target);
    }

    @Transactional(rollbackFor = Exception.class)
    @Validated(Insert.class)
    public <S extends ENTITY> S insert(@Valid S entity) {
        return repository.save(entity);
    }

    @Transactional(rollbackFor = Exception.class)
    @Validated(Default.class)
    public <S extends ENTITY> S update(@Valid S entity) {
        S target = (S) this.findById(entity.getPrimaryKey()).orElseThrow(() -> new NoSuchElementException("cann't find update target from database"));
        BeanUtils.copyProperties(entity, target, getNullPropertyNames(entity));
        return repository.save(target);
    }

    /**
     * Saves all given entities.
     *
     * @param entities must not be {@literal null} nor must it contain {@literal null}.
     * @return the saved entities; will never be {@literal null}. The returned {@literal Iterable} will have the same size
     * as the {@literal Iterable} passed as an argument.
     * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is
     *                                  {@literal null}.
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public <S extends ENTITY> Iterable<S> saveAll(Iterable<S> entities) {
        return repository.saveAll(entities);
    }

    /**
     * Retrieves an entity by its id.
     *
     * @param id must not be {@literal null}.
     * @return the entity with the given id or {@literal Optional#empty()} if none found.
     * @throws IllegalArgumentException if {@literal id} is {@literal null}.
     */
    @Transactional(readOnly = true)
    @Override
    public Optional<ENTITY> findById(ID id) {
        return repository.findById(id);
    }

    /**
     * Returns whether an entity with the given id exists.
     *
     * @param id must not be {@literal null}.
     * @return {@literal true} if an entity with the given id exists, {@literal false} otherwise.
     * @throws IllegalArgumentException if {@literal id} is {@literal null}.
     */
    @Transactional(readOnly = true)
    @Override
    public boolean existsById(ID id) {
        return repository.existsById(id);
    }

    /**
     * Returns all instances of the type.
     *
     * @return all entities
     */
    @Transactional(readOnly = true)
    @Override
    public Iterable<ENTITY> findAll() {
        return repository.findAll();
    }

    /**
     * Returns all instances of the type {@code T} with the given IDs.
     * <p>
     * If some or all ids are not found, no entities are returned for these IDs.
     * <p>
     * Note that the order of elements in the result is not guaranteed.
     *
     * @param ids must not be {@literal null} nor contain any {@literal null} values.
     * @return guaranteed to be not {@literal null}. The size can be equal or less than the number of given
     * {@literal ids}.
     * @throws IllegalArgumentException in case the given {@link Iterable ids} or one of its items is {@literal null}.
     */
    @Transactional(readOnly = true)
    @Override
    public Iterable<ENTITY> findAllById(Iterable<ID> ids) {
        return repository.findAllById(ids);
    }

    /**
     * Returns the number of entities available.
     *
     * @return the number of entities.
     */
    @Transactional(readOnly = true)
    @Override
    public long count() {
        return repository.count();
    }

    /**
     * Deletes the entity with the given id.
     *
     * @param id must not be {@literal null}.
     * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deleteById(ID id) {
        repository.deleteById(id);
    }

    /**
     * Deletes a given entity.
     *
     * @param entity must not be {@literal null}.
     * @throws IllegalArgumentException in case the given entity is {@literal null}.
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void delete(ENTITY entity) {
        repository.delete(entity);
    }

    /**
     * Deletes the given entities.
     *
     * @param entities must not be {@literal null}. Must not contain {@literal null} elements.
     * @throws IllegalArgumentException in case the given {@literal entities} or one of its entities is {@literal null}.
     */
    @Override
    public void deleteAll(Iterable<? extends ENTITY> entities) {
        repository.deleteAll(entities);
    }

    /**
     * Deletes all entities managed by the repository.
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deleteAll() {
        repository.deleteAll();
    }

    /**
     * Returns a single entity matching the given {@link Example} or {@literal null} if none was found.
     *
     * @param example must not be {@literal null}.
     * @return a single entity matching the given {@link Example} or {@link Optional#empty()} if none was found.
     * @throws IncorrectResultSizeDataAccessException if the Example yields more than one result.
     */
    @Transactional(readOnly = true)
    @Override
    public <S extends ENTITY> Optional<S> findOne(Example<S> example) {
        return repository.findOne(example);
    }

    /**
     * Returns all entities matching the given {@link Example}. In case no match could be found an empty {@link Iterable}
     * is returned.
     *
     * @param example must not be {@literal null}.
     * @return all entities matching the given {@link Example}.
     */
    @Transactional(readOnly = true)
    @Override
    public <S extends ENTITY> Iterable<S> findAll(Example<S> example) {
        return repository.findAll(example);
    }

    /**
     * Returns all entities matching the given {@link Example} applying the given {@link Sort}. In case no match could be
     * found an empty {@link Iterable} is returned.
     *
     * @param example must not be {@literal null}.
     * @param sort    the {@link Sort} specification to sort the results by, must not be {@literal null}.
     * @return all entities matching the given {@link Example}.
     * @since 1.10
     */
    @Transactional(readOnly = true)
    @Override
    public <S extends ENTITY> Iterable<S> findAll(Example<S> example, Sort sort) {
        return repository.findAll(example, sort);
    }

    /**
     * Returns a {@link Page} of entities matching the given {@link Example}. In case no match could be found, an empty
     * {@link Page} is returned.
     *
     * @param example  must not be {@literal null}.
     * @param pageable can be {@literal null}.
     * @return a {@link Page} of entities matching the given {@link Example}.
     */
    @Transactional(readOnly = true)
    @Override
    public <S extends ENTITY> Page<S> findAll(Example<S> example, Pageable pageable) {
        return repository.findAll(example, pageable);
    }

    /**
     * Returns the number of instances matching the given {@link Example}.
     *
     * @param example the {@link Example} to count instances for. Must not be {@literal null}.
     * @return the number of instances matching the {@link Example}.
     */
    @Override
    public <S extends ENTITY> long count(Example<S> example) {
        return repository.count(example);
    }

    /**
     * Checks whether the data store contains elements that match the given {@link Example}.
     *
     * @param example the {@link Example} to use for the existence check. Must not be {@literal null}.
     * @return {@literal true} if the data store contains elements that match the given {@link Example}.
     */
    @Override
    public <S extends ENTITY> boolean exists(Example<S> example) {
        return repository.exists(example);
    }

    @Transactional(readOnly = true)
    @Override
    public Optional<ENTITY> findOne(Specification<ENTITY> specification) {
        return repository.findOne(specification);
    }

    @Transactional(readOnly = true)
    @Override
    public List<ENTITY> findAll(Specification<ENTITY> specification) {
        return repository.findAll(specification);
    }

    @Transactional(readOnly = true)
    @Override
    public Page<ENTITY> findAll(Specification<ENTITY> specification, Pageable pageable) {
        return repository.findAll(specification, pageable);
    }

    // will not execute sql count
    @Transactional(readOnly = true)
    public Slice<ENTITY> findAllSliced(Specification<ENTITY> specification, Pageable pageable) {
        return repository.findAllSliced(specification, pageable);
    }

    @Transactional(readOnly = true)
    @Override
    public List<ENTITY> findAll(Specification<ENTITY> specification, Sort sort) {
        return repository.findAll(specification, sort);
    }

    @Transactional(readOnly = true)
    @Override
    public long count(Specification<ENTITY> specification) {
        return repository.count(specification);
    }
}
